Micron Document
NexusPi Git Node

Commit de1e16a2d30ef7aac0b4e279cc4d42a32c7f1ead


Parents : c84006d
Author : copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-04-25T00:15:26Z
Committer : GitHub <noreply@github.com>
Date : 2026-04-25T00:15:26Z

Add device-advertisement options to RTNode config portal

Agent-Logs-Url: https://github.com/jrl290/RTNode-HeltecV4/sessions/89412bfb-863f-4631-aa8d-1b3ceed34fbf

Co-authored-by: jrl290 <1811031+jrl290@users.noreply.github.com>

Changes

3 files changed, 215 insertions(+), 1 deletions(-)

M BoundaryMode.h +83 -1
M README.md +11

Diff

diff --git a/BoundaryConfig.h b/BoundaryConfig.h
index a846d59..7b37e52 100755
--- a/BoundaryConfig.h
+++ b/BoundaryConfig.h
@@ -322,6 +322,96 @@ static void config_send_html() {
html += String(boundary_state.ifac_passphrase);
html += F("'>");
+ // ── Device Advertisement Section ──
+ html += F(
+ "<h2>&#x1f4cd; Device Advertisement</h2>"
+ "<p class='note'>Advertise this node and its parameters on the Reticulum network. "
+ "Maps such as <a href='https://rmap.world' target='_blank' style='color:#7ecfff'>rmap.world</a> "
+ "use these announcements to automatically place a pin for the node. "
+ "Disabled by default; enable only if you want this node to be publicly listed.</p>"
+ "<label>Advertise Device</label>"
+ "<select name='advert_en'>"
+ );
+ html += F("<option value='0'");
+ if (!boundary_state.advert_enabled) html += F(" selected");
+ html += F(">Disabled</option>");
+ html += F("<option value='1'");
+ if (boundary_state.advert_enabled) html += F(" selected");
+ html += F(">Enabled</option>");
+ html += F("</select>");
+
+ // Latitude / Longitude — pre-populate with current values, but keep
+ // the inputs blank when the user has not yet set coordinates so the
+ // browser placeholder hint is visible.
+ char lat_str[32];
+ char lon_str[32];
+ lat_str[0] = '\0';
+ lon_str[0] = '\0';
+ if (boundary_state.advert_enabled ||
+ boundary_state.advert_lat != 0.0 ||
+ boundary_state.advert_lon != 0.0) {
+ dtostrf(boundary_state.advert_lat, 1, 6, lat_str);
+ dtostrf(boundary_state.advert_lon, 1, 6, lon_str);
+ }
+
+ html += F("<div class='row'>");
+ html += F("<div><label>Latitude (&deg;)</label>");
+ html += F("<input id='advert_lat' name='advert_lat' type='text' inputmode='decimal' "
+ "placeholder='e.g. 37.774929' value='");
+ html += String(lat_str);
+ html += F("'></div>");
+ html += F("<div><label>Longitude (&deg;)</label>");
+ html += F("<input id='advert_lon' name='advert_lon' type='text' inputmode='decimal' "
+ "placeholder='e.g. -122.419416' value='");
+ html += String(lon_str);
+ html += F("'></div>");
+ html += F("</div>");
+ html += F("<p class='note'>Decimal degrees, signed. North/East positive, South/West negative. "
+ "Leave both blank to omit coordinates.</p>");
+
+ // Browser geolocation helper button. Geolocation may be blocked on
+ // plain HTTP origins by some browsers; the script reports an error
+ // inline if the request fails or is denied.
+ html += F(
+ "<button type='button' id='geo_btn' "
+ "style='width:100%;padding:10px;margin:4px 0 6px;background:#0f3460;"
+ "color:#fff;border:none;border-radius:4px;font-size:0.95em;cursor:pointer;'>"
+ "&#x1f4cd; Use Browser Location</button>"
+ "<p id='geo_status' class='note' style='min-height:1em;'></p>"
+ "<script>"
+ "(function(){"
+ "var btn=document.getElementById('geo_btn');"
+ "var st=document.getElementById('geo_status');"
+ "if(!btn)return;"
+ "btn.addEventListener('click',function(){"
+ "if(!('geolocation' in navigator)){"
+ "st.textContent='Geolocation not supported by this browser.';return;}"
+ "st.textContent='Requesting location\\u2026';"
+ "navigator.geolocation.getCurrentPosition(function(pos){"
+ "document.getElementById('advert_lat').value=pos.coords.latitude.toFixed(6);"
+ "document.getElementById('advert_lon').value=pos.coords.longitude.toFixed(6);"
+ "st.textContent='Location filled (\\u00b1'+Math.round(pos.coords.accuracy)+' m).';"
+ "},function(err){"
+ "st.textContent='Could not get location: '+err.message;"
+ "},{enableHighAccuracy:true,timeout:15000,maximumAge:0});"
+ "});"
+ "})();"
+ "</script>"
+ );
+
+ html += F("<label>Randomize Offset</label>"
+ "<select name='advert_jitter'>");
+ html += F("<option value='0'");
+ if (!boundary_state.advert_jitter) html += F(" selected");
+ html += F(">Disabled</option>");
+ html += F("<option value='1'");
+ if (boundary_state.advert_jitter) html += F(" selected");
+ html += F(">Enabled (~0.5 km / 0.5 mi)</option>");
+ html += F("</select>");
+ html += F("<p class='note'>When enabled, the advertised coordinates are shifted by a "
+ "random offset of approximately half a kilometre (about half a mile) for "
+ "privacy. The exact stored coordinates are not changed.</p>");
+
// ── Options Section ──
html += F(
"<h2>&#x2699; Options</h2>"
@@ -473,6 +563,37 @@ static void config_handle_save() {
boundary_state.ifac_enabled = false;
}
+ // ── Device advertisement settings ──
+ boundary_state.advert_enabled = (config_server->arg("advert_en").toInt() == 1);
+
+ // Empty lat/lon strings are treated as "not set" → 0.0. Otherwise parse
+ // and clamp to valid ranges; out-of-range values are silently coerced
+ // to 0.0 rather than rejecting the whole save.
+ String lat_arg = config_server->arg("advert_lat");
+ String lon_arg = config_server->arg("advert_lon");
+ lat_arg.trim();
+ lon_arg.trim();
+ if (lat_arg.length() == 0) {
+ boundary_state.advert_lat = 0.0;
+ } else {
+ double lat_val = lat_arg.toDouble();
+ if (lat_val < -90.0 || lat_val > 90.0 || !(lat_val == lat_val)) {
+ lat_val = 0.0;
+ }
+ boundary_state.advert_lat = lat_val;
+ }
+ if (lon_arg.length() == 0) {
+ boundary_state.advert_lon = 0.0;
+ } else {
+ double lon_val = lon_arg.toDouble();
+ if (lon_val < -180.0 || lon_val > 180.0 || !(lon_val == lon_val)) {
+ lon_val = 0.0;
+ }
+ boundary_state.advert_lon = lon_val;
+ }
+
+ boundary_state.advert_jitter = (config_server->arg("advert_jitter").toInt() == 1);
+
// Save boundary config to EEPROM
boundary_save_config();

diff --git a/BoundaryMode.h b/BoundaryMode.h
index 3934dfd..63465f5 100755
--- a/BoundaryMode.h
+++ b/BoundaryMode.h
@@ -88,7 +88,16 @@
#define ADDR_CONF_APP_MARKER0 0x119 // RTNode app marker byte 0
#define ADDR_CONF_APP_MARKER1 0x11A // RTNode app marker byte 1
#define ADDR_CONF_APP_VERSION 0x11B // RTNode app config version
-// Total: 0x11C (284 bytes — extends beyond 256-byte CONFIG area into
+// Device advertisement settings (advertise this node's parameters and
+// optional GPS coordinates to the Reticulum network so external maps such
+// as rmap.world can pin it). Stored after the app version markers so the
+// existing layout is preserved and old saves keep working (uninitialised
+// 0xFF bytes are interpreted as "advertisement disabled, no coordinates").
+#define ADDR_CONF_ADVERT_EN 0x11C // Advertise enable flag (1 byte, 0x73 = enabled)
+#define ADDR_CONF_ADVERT_LAT 0x11D // Latitude as IEEE-754 double (8 bytes, host byte order)
+#define ADDR_CONF_ADVERT_LON 0x125 // Longitude as IEEE-754 double (8 bytes, host byte order)
+#define ADDR_CONF_ADVERT_JITTER 0x12D // Randomize ~0.5 km offset flag (1 byte, 0x73 = enabled)
+// Total: 0x12E (302 bytes — extends beyond 256-byte CONFIG area into
// unused EEPROM gap; safe on ESP32 where EEPROM starts at 824)
#define BOUNDARY_ENABLE_BYTE 0x73
@@ -118,6 +127,14 @@ struct BoundaryState {
char ifac_netname[33]; // Network name (empty = not set)
char ifac_passphrase[33]; // Passphrase (empty = not set)
+ // Device advertisement settings (used to advertise this node's
+ // parameters and optional GPS location so external maps such as
+ // rmap.world can pin this node). Defaults to disabled.
+ bool advert_enabled; // Whether to advertise this device
+ double advert_lat; // Latitude in decimal degrees (-90..90)
+ double advert_lon; // Longitude in decimal degrees (-180..180)
+ bool advert_jitter; // Randomize ~0.5 km offset for advertised coords
+
// Runtime state
bool wifi_connected;
bool tcp_connected; // Backbone (WAN) connected
@@ -152,6 +169,31 @@ inline void boundary_clear_app_marker() {
EEPROM.commit();
}
+// ─── Helpers for serialising IEEE-754 doubles to EEPROM ──────────────────────
+// Stored as 8 raw bytes in EEPROM order. If all 8 bytes read back as 0xFF
+// (uninitialised), the value is treated as "not set" and a default is used.
+inline void boundary_write_double(int addr, double value) {
+ uint8_t buf[8];
+ memcpy(buf, &value, sizeof(buf));
+ for (int i = 0; i < 8; i++) {
+ EEPROM.write(config_addr(addr + i), buf[i]);
+ }
+}
+
+inline bool boundary_read_double(int addr, double& out) {
+ uint8_t buf[8];
+ bool all_ff = true;
+ for (int i = 0; i < 8; i++) {
+ buf[i] = EEPROM.read(config_addr(addr + i));
+ if (buf[i] != 0xFF) all_ff = false;
+ }
+ if (all_ff) return false;
+ memcpy(&out, buf, sizeof(out));
+ // Reject NaN / inf values that may slip in from corrupted EEPROM.
+ if (!(out == out) || out > 1e9 || out < -1e9) return false;
+ return true;
+}
+
inline void boundary_load_config() {
// Check if boundary mode is configured
uint8_t bmode = EEPROM.read(config_addr(ADDR_CONF_BMODE));
@@ -173,6 +215,10 @@ inline void boundary_load_config() {
boundary_state.ifac_enabled = false;
boundary_state.ifac_netname[0] = '\0';
boundary_state.ifac_passphrase[0] = '\0';
+ boundary_state.advert_enabled = false;
+ boundary_state.advert_lat = 0.0;
+ boundary_state.advert_lon = 0.0;
+ boundary_state.advert_jitter = false;
// Mark as enabled since we're compiled with BOUNDARY_MODE
boundary_state.enabled = true;
return;
@@ -247,6 +293,33 @@ inline void boundary_load_config() {
}
boundary_state.ifac_passphrase[32] = '\0';
+ // Load device advertisement settings. Defaults to disabled for both
+ // the master toggle and the location jitter, so previously saved
+ // configurations (which have 0xFF in these slots) keep advertising
+ // off until the user explicitly enables it from the portal.
+ {
+ uint8_t advert_en_byte = EEPROM.read(config_addr(ADDR_CONF_ADVERT_EN));
+ boundary_state.advert_enabled = (advert_en_byte == BOUNDARY_ENABLE_BYTE);
+
+ if (!boundary_read_double(ADDR_CONF_ADVERT_LAT, boundary_state.advert_lat)) {
+ boundary_state.advert_lat = 0.0;
+ }
+ if (!boundary_read_double(ADDR_CONF_ADVERT_LON, boundary_state.advert_lon)) {
+ boundary_state.advert_lon = 0.0;
+ }
+
+ // Clamp to valid ranges in case of corrupted EEPROM data.
+ if (boundary_state.advert_lat < -90.0 || boundary_state.advert_lat > 90.0) {
+ boundary_state.advert_lat = 0.0;
+ }
+ if (boundary_state.advert_lon < -180.0 || boundary_state.advert_lon > 180.0) {
+ boundary_state.advert_lon = 0.0;
+ }
+
+ uint8_t advert_jitter_byte = EEPROM.read(config_addr(ADDR_CONF_ADVERT_JITTER));
+ boundary_state.advert_jitter = (advert_jitter_byte == BOUNDARY_ENABLE_BYTE);
+ }
+
// Reset runtime state
boundary_state.packets_bridged_lora_to_tcp = 0;
boundary_state.packets_bridged_tcp_to_lora = 0;
@@ -295,6 +368,15 @@ inline void boundary_save_config() {
EEPROM.write(config_addr(ADDR_CONF_IFAC_PASS + i), boundary_state.ifac_passphrase[i]);
}
EEPROM.write(config_addr(ADDR_CONF_IFAC_PASS + 32), 0x00);
+
+ // Device advertisement settings
+ EEPROM.write(config_addr(ADDR_CONF_ADVERT_EN),
+ boundary_state.advert_enabled ? BOUNDARY_ENABLE_BYTE : 0x00);
+ boundary_write_double(ADDR_CONF_ADVERT_LAT, boundary_state.advert_lat);
+ boundary_write_double(ADDR_CONF_ADVERT_LON, boundary_state.advert_lon);
+ EEPROM.write(config_addr(ADDR_CONF_ADVERT_JITTER),
+ boundary_state.advert_jitter ? BOUNDARY_ENABLE_BYTE : 0x00);
+
EEPROM.write(config_addr(ADDR_CONF_APP_MARKER0), BOUNDARY_APP_MARKER0);
EEPROM.write(config_addr(ADDR_CONF_APP_MARKER1), BOUNDARY_APP_MARKER1);
EEPROM.write(config_addr(ADDR_CONF_APP_VERSION), BOUNDARY_APP_VERSION);

diff --git a/README.md b/README.md
index 6b54728..a792041 100755
--- a/README.md
+++ b/README.md
@@ -161,6 +161,17 @@ The web form has four sections:
| **Coding Rate** | 4/5 – 4/8 |
| **TX Power** | 2 – 28 dBm |
+#### 📍 Device Advertisement
+Optional: announce this node and its parameters on the Reticulum network so external maps such as [rmap.world](https://rmap.world) can automatically place a pin for it. **Disabled by default** — only enable if you want this node to be publicly listed.
+
+| Field | Description |
+|-------|-------------|
+| **Advertise Device** | Enable/Disable advertising this node's parameters |
+| **Latitude** | GPS latitude in decimal degrees (e.g. `37.774929`). North positive, South negative. Leave blank to omit |
+| **Longitude** | GPS longitude in decimal degrees (e.g. `-122.419416`). East positive, West negative. Leave blank to omit |
+| **Use Browser Location** | Button that fills the latitude/longitude fields from the browser's geolocation service (requires user permission; some browsers block it on plain HTTP origins) |
+| **Randomize Offset** | When enabled, the advertised coordinates are shifted by a random offset of approximately half a kilometre (about half a mile) for privacy. The exact stored coordinates are not changed |
+
After saving, the device reboots with the new configuration applied.
## OLED Display Layout

Served by rngit 1.4.2 - Generated in 0.03s